# This code is part of Qiskit.
#
# (C) Copyright IBM 2017.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
A generic quantum instruction.

Instructions can be implementable on hardware (u, cx, etc.) or in simulation
(snapshot, noise, etc.).

Instructions can be unitary (a.k.a Gate) or non-unitary.

Instructions are identified by the following:

    name: A string to identify the type of instruction.
          Used to request a specific instruction on the backend, or in visualizing circuits.

    num_qubits, num_clbits: dimensions of the instruction.

    params: List of parameters to specialize a specific instruction instance.

Instructions do not have any context about where they are in a circuit (which qubits/clbits).
The circuit itself keeps this context.
"""

import copy
from itertools import zip_longest
from typing import List

import numpy

from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.qobj.qasm_qobj import QasmQobjInstruction
from qiskit.circuit.parameter import ParameterExpression
from qiskit.circuit.operation import Operation
from qiskit.utils.deprecation import deprecate_func
from .tools import pi_check

_CUTOFF_PRECISION = 1e-10


class Instruction(Operation):
    """Generic quantum instruction."""

    # Class attribute to treat like barrier for transpiler, unroller, drawer
    # NOTE: Using this attribute may change in the future (See issue # 5811)
    _directive = False

    def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None):
        """Create a new instruction.

        Args:
            name (str): instruction name
            num_qubits (int): instruction's qubit width
            num_clbits (int): instruction's clbit width
            params (list[int|float|complex|str|ndarray|list|ParameterExpression]):
                list of parameters
            duration (int or float): instruction's duration. it must be integer if ``unit`` is 'dt'
            unit (str): time unit of duration
            label (str or None): An optional label for identifying the instruction.

        Raises:
            CircuitError: when the register is not in the correct format.
            TypeError: when the optional label is provided, but it is not a string.
        """
        if not isinstance(num_qubits, int) or not isinstance(num_clbits, int):
            raise CircuitError("num_qubits and num_clbits must be integer.")
        if num_qubits < 0 or num_clbits < 0:
            raise CircuitError(
                "bad instruction dimensions: %d qubits, %d clbits." % num_qubits, num_clbits
            )
        self._name = name
        self._num_qubits = num_qubits
        self._num_clbits = num_clbits

        self._params = []  # a list of gate params stored
        # Custom instruction label
        # NOTE: The conditional statement checking if the `_label` attribute is
        #       already set is a temporary work around that can be removed after
        #       the next stable qiskit-aer release
        if not hasattr(self, "_label"):
            if label is not None and not isinstance(label, str):
                raise TypeError("label expects a string or None")
            self._label = label
        # tuple (ClassicalRegister, int), tuple (Clbit, bool) or tuple (Clbit, int)
        # when the instruction has a conditional ("if")
        self.condition = None
        # list of instructions (and their contexts) that this instruction is composed of
        # empty definition means opaque or fundamental instruction
        self._definition = None

        self._duration = duration
        self._unit = unit

        self.params = params  # must be at last (other properties may be required for validation)

    def __eq__(self, other):
        """Two instructions are the same if they have the same name,
        same dimensions, and same params.

        Args:
            other (instruction): other instruction

        Returns:
            bool: are self and other equal.
        """
        if (
            type(self) is not type(other)
            or self.name != other.name
            or self.num_qubits != other.num_qubits
            or self.num_clbits != other.num_clbits
            or self.definition != other.definition
        ):
            return False

        for self_param, other_param in zip_longest(self.params, other.params):
            try:
                if self_param == other_param:
                    continue
            except ValueError:
                pass

            try:
                self_asarray = numpy.asarray(self_param)
                other_asarray = numpy.asarray(other_param)
                if numpy.shape(self_asarray) == numpy.shape(other_asarray) and numpy.allclose(
                    self_param, other_param, atol=_CUTOFF_PRECISION, rtol=0
                ):
                    continue
            except (ValueError, TypeError):
                pass

            try:
                if numpy.isclose(
                    float(self_param), float(other_param), atol=_CUTOFF_PRECISION, rtol=0
                ):
                    continue
            except TypeError:
                pass

            return False

        return True

    def __repr__(self) -> str:
        """Generates a representation of the Intruction object instance
        Returns:
            str: A representation of the Instruction instance with the name,
                 number of qubits, classical bits and params( if any )
        """
        return "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format(
            self.name, self.num_qubits, self.num_clbits, self.params
        )

    def soft_compare(self, other: "Instruction") -> bool:
        """
        Soft comparison between gates. Their names, number of qubits, and classical
        bit numbers must match. The number of parameters must match. Each parameter
        is compared. If one is a ParameterExpression then it is not taken into
        account.

        Args:
            other (instruction): other instruction.

        Returns:
            bool: are self and other equal up to parameter expressions.
        """
        if (
            self.name != other.name
            or other.num_qubits != other.num_qubits
            or other.num_clbits != other.num_clbits
            or len(self.params) != len(other.params)
        ):
            return False

        for self_param, other_param in zip_longest(self.params, other.params):
            if isinstance(self_param, ParameterExpression) or isinstance(
                other_param, ParameterExpression
            ):
                continue
            if isinstance(self_param, numpy.ndarray) and isinstance(other_param, numpy.ndarray):
                if numpy.shape(self_param) == numpy.shape(other_param) and numpy.allclose(
                    self_param, other_param, atol=_CUTOFF_PRECISION
                ):
                    continue
            else:
                try:
                    if numpy.isclose(self_param, other_param, atol=_CUTOFF_PRECISION):
                        continue
                except TypeError:
                    pass

            return False

        return True

    def _define(self):
        """Populates self.definition with a decomposition of this gate."""
        pass

    @property
    def params(self):
        """return instruction params."""
        return self._params

    @params.setter
    def params(self, parameters):
        self._params = []
        for single_param in parameters:
            if isinstance(single_param, ParameterExpression):
                self._params.append(single_param)
            else:
                self._params.append(self.validate_parameter(single_param))

    def validate_parameter(self, parameter):
        """Instruction parameters has no validation or normalization."""
        return parameter

    def is_parameterized(self):
        """Return True .IFF. instruction is parameterized else False"""
        return any(
            isinstance(param, ParameterExpression) and param.parameters for param in self.params
        )

    @property
    def definition(self):
        """Return definition in terms of other basic gates."""
        if self._definition is None:
            self._define()
        return self._definition

    @definition.setter
    def definition(self, array):
        """Set gate representation"""
        self._definition = array

    @property
    def decompositions(self):
        """Get the decompositions of the instruction from the SessionEquivalenceLibrary."""
        # pylint: disable=cyclic-import
        from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel

        return sel.get_entry(self)

    @decompositions.setter
    def decompositions(self, decompositions):
        """Set the decompositions of the instruction from the SessionEquivalenceLibrary."""
        # pylint: disable=cyclic-import
        from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel

        sel.set_entry(self, decompositions)

    def add_decomposition(self, decomposition):
        """Add a decomposition of the instruction to the SessionEquivalenceLibrary."""
        # pylint: disable=cyclic-import
        from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel

        sel.add_equivalence(self, decomposition)

    @property
    def duration(self):
        """Get the duration."""
        return self._duration

    @duration.setter
    def duration(self, duration):
        """Set the duration."""
        self._duration = duration

    @property
    def unit(self):
        """Get the time unit of duration."""
        return self._unit

    @unit.setter
    def unit(self, unit):
        """Set the time unit of duration."""
        self._unit = unit

    def assemble(self):
        """Assemble a QasmQobjInstruction"""
        instruction = QasmQobjInstruction(name=self.name)
        # Evaluate parameters
        if self.params:
            params = [x.evalf(x) if hasattr(x, "evalf") else x for x in self.params]
            instruction.params = params
        # Add placeholder for qarg and carg params
        if self.num_qubits:
            instruction.qubits = list(range(self.num_qubits))
        if self.num_clbits:
            instruction.memory = list(range(self.num_clbits))
        # Add label if defined
        if self.label:
            instruction.label = self.label
        # Add condition parameters for assembler. This is needed to convert
        # to a qobj conditional instruction at assemble time and after
        # conversion will be deleted by the assembler.
        if self.condition:
            instruction._condition = self.condition
        return instruction

    @property
    def label(self) -> str:
        """Return instruction label"""
        return self._label

    @label.setter
    def label(self, name: str):
        """Set instruction label to name

        Args:
            name (str or None): label to assign instruction

        Raises:
            TypeError: name is not string or None.
        """
        if isinstance(name, (str, type(None))):
            self._label = name
        else:
            raise TypeError("label expects a string or None")

    def reverse_ops(self):
        """For a composite instruction, reverse the order of sub-instructions.

        This is done by recursively reversing all sub-instructions.
        It does not invert any gate.

        Returns:
            qiskit.circuit.Instruction: a new instruction with
                sub-instructions reversed.
        """
        if not self._definition:
            return self.copy()

        reverse_inst = self.copy(name=self.name + "_reverse")
        reversed_definition = self._definition.copy_empty_like()
        for inst in reversed(self._definition):
            reversed_definition.append(inst.operation.reverse_ops(), inst.qubits, inst.clbits)
        reverse_inst.definition = reversed_definition
        return reverse_inst

    def inverse(self):
        """Invert this instruction.

        If the instruction is composite (i.e. has a definition),
        then its definition will be recursively inverted.

        Special instructions inheriting from Instruction can
        implement their own inverse (e.g. T and Tdg, Barrier, etc.)

        Returns:
            qiskit.circuit.Instruction: a fresh instruction for the inverse

        Raises:
            CircuitError: if the instruction is not composite
                and an inverse has not been implemented for it.
        """
        if self.definition is None:
            raise CircuitError("inverse() not implemented for %s." % self.name)

        from qiskit.circuit import Gate  # pylint: disable=cyclic-import

        if self.name.endswith("_dg"):
            name = self.name[:-3]
        else:
            name = self.name + "_dg"
        if self.num_clbits:
            inverse_gate = Instruction(
                name=name,
                num_qubits=self.num_qubits,
                num_clbits=self.num_clbits,
                params=self.params.copy(),
            )

        else:
            inverse_gate = Gate(name=name, num_qubits=self.num_qubits, params=self.params.copy())

        inverse_definition = self._definition.copy_empty_like()
        inverse_definition.global_phase = -inverse_definition.global_phase
        for inst in reversed(self._definition):
            inverse_definition._append(inst.operation.inverse(), inst.qubits, inst.clbits)
        inverse_gate.definition = inverse_definition
        return inverse_gate

    def c_if(self, classical, val):
        """Set a classical equality condition on this instruction between the register or cbit
        ``classical`` and value ``val``.

        .. note::

            This is a setter method, not an additive one.  Calling this multiple times will silently
            override any previously set condition; it does not stack.
        """
        if not isinstance(classical, (ClassicalRegister, Clbit)):
            raise CircuitError("c_if must be used with a classical register or classical bit")
        if val < 0:
            raise CircuitError("condition value should be non-negative")
        if isinstance(classical, Clbit):
            # Casting the conditional value as Boolean when
            # the classical condition is on a classical bit.
            val = bool(val)
        self.condition = (classical, val)
        return self

    def copy(self, name=None):
        """
        Copy of the instruction.

        Args:
            name (str): name to be given to the copied circuit, if ``None`` then the name stays the same.

        Returns:
            qiskit.circuit.Instruction: a copy of the current instruction, with the name updated if it
            was provided
        """
        cpy = self.__deepcopy__()

        if name:
            cpy.name = name
        return cpy

    def __deepcopy__(self, _memo=None):
        cpy = copy.copy(self)
        cpy._params = copy.copy(self._params)
        if self._definition:
            cpy._definition = copy.deepcopy(self._definition, _memo)
        return cpy

    def _qasmif(self, string):
        """Print an if statement if needed."""
        from qiskit.qasm2 import QASM2ExportError  # pylint: disable=cyclic-import

        if self.condition is None:
            return string
        if not isinstance(self.condition[0], ClassicalRegister):
            raise QASM2ExportError(
                "OpenQASM 2 can only condition on registers, but got '{self.condition[0]}'"
            )
        return "if(%s==%d) " % (self.condition[0].name, self.condition[1]) + string

    @deprecate_func(
        additional_msg=(
            "Correct exporting to OpenQASM 2 is the responsibility of a larger exporter; it cannot "
            "safely be done on an object-by-object basis without context. No replacement will be "
            "provided, because the premise is wrong."
        ),
        since="0.25.0",
    )
    def qasm(self):
        """Return a default OpenQASM string for the instruction.

        Derived instructions may override this to print in a
        different format (e.g. measure q[0] -> c[0];).
        """
        name_param = self.name
        if self.params:
            name_param = "{}({})".format(
                name_param,
                ",".join([pi_check(i, output="qasm", eps=1e-12) for i in self.params]),
            )

        return self._qasmif(name_param)

    def broadcast_arguments(self, qargs, cargs):
        """
        Validation of the arguments.

        Args:
            qargs (List): List of quantum bit arguments.
            cargs (List): List of classical bit arguments.

        Yields:
            Tuple(List, List): A tuple with single arguments.

        Raises:
            CircuitError: If the input is not valid. For example, the number of
                arguments does not match the gate expectation.
        """
        if len(qargs) != self.num_qubits:
            raise CircuitError(
                f"The amount of qubit arguments {len(qargs)} does not match"
                f" the instruction expectation ({self.num_qubits})."
            )
        if len(cargs) != self.num_clbits:
            raise CircuitError(
                f"The amount of clbit arguments {len(cargs)} does not match"
                f" the instruction expectation ({self.num_clbits})."
            )

        #  [[q[0], q[1]], [c[0], c[1]]] -> [q[0], c[0]], [q[1], c[1]]
        flat_qargs = [qarg for sublist in qargs for qarg in sublist]
        flat_cargs = [carg for sublist in cargs for carg in sublist]
        yield flat_qargs, flat_cargs

    def _return_repeat(self, exponent):
        return Instruction(
            name=f"{self.name}*{exponent}",
            num_qubits=self.num_qubits,
            num_clbits=self.num_clbits,
            params=self.params,
        )

    def repeat(self, n):
        """Creates an instruction with `gate` repeated `n` amount of times.

        Args:
            n (int): Number of times to repeat the instruction

        Returns:
            qiskit.circuit.Instruction: Containing the definition.

        Raises:
            CircuitError: If n < 1.
        """
        if int(n) != n or n < 1:
            raise CircuitError("Repeat can only be called with strictly positive integer.")

        n = int(n)

        instruction = self._return_repeat(n)
        qargs = [] if self.num_qubits == 0 else QuantumRegister(self.num_qubits, "q")
        cargs = [] if self.num_clbits == 0 else ClassicalRegister(self.num_clbits, "c")

        if instruction.definition is None:
            # pylint: disable=cyclic-import
            from qiskit.circuit import QuantumCircuit, CircuitInstruction

            qc = QuantumCircuit()
            if qargs:
                qc.add_register(qargs)
            if cargs:
                qc.add_register(cargs)
            circuit_instruction = CircuitInstruction(self, qargs, cargs)
            for _ in [None] * n:
                qc._append(circuit_instruction)
        instruction.definition = qc
        return instruction

    @property
    def condition_bits(self) -> List[Clbit]:
        """Get Clbits in condition."""
        if self.condition is None:
            return []
        if isinstance(self.condition[0], Clbit):
            return [self.condition[0]]
        else:  # ClassicalRegister
            return list(self.condition[0])

    @property
    def name(self):
        """Return the name."""
        return self._name

    @name.setter
    def name(self, name):
        """Set the name."""
        self._name = name

    @property
    def num_qubits(self):
        """Return the number of qubits."""
        return self._num_qubits

    @num_qubits.setter
    def num_qubits(self, num_qubits):
        """Set num_qubits."""
        self._num_qubits = num_qubits

    @property
    def num_clbits(self):
        """Return the number of clbits."""
        return self._num_clbits

    @num_clbits.setter
    def num_clbits(self, num_clbits):
        """Set num_clbits."""
        self._num_clbits = num_clbits
